{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "81a6f12e",
   "metadata": {},
   "source": [
    "下面几段代码展示朴素贝叶斯模型的训练和预测。这里使用的数据集为本书自制的Books数据集，包含约1万本图书的标题，分为3种主题。首先是预处理，针对文本分类的预处理主要包含以下步骤：\n",
    "\n",
    "- 通常可以将英文文本全部转换为小写，或者将中文内容全部转换为简体，等等，这一般不会改变文本内容。\n",
    "- 去除标点。英文中的标点符号和单词之间没有空格（如——“Hi, there!”），如果不去除标点，“Hi,”和“there!”会被识别为不同于“Hi”和“there”的两个词，这显然是不合理的。对于中文，移除标点一般也不会影响文本的内容。\n",
    "- 分词。中文汉字之间没有空格分隔，中文分词有时比英文分词更加困难，此处不再赘述。\n",
    "- 去除停用词（如“I”、“is”、“的”等）。这些词往往大量出现但没有具体含义。\n",
    "- 建立词表。通常会忽略语料库中频率非常低的词。\n",
    "- 将词转换为词表索引（ID），便于机器学习模型使用。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "8cea9b5c",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Processing c:\\users\\31873\\appdata\\local\\temp\\microsoftedgedownloads\\dba00908-4796-4f7d-8a1b-37a25065bb6b\\zh_core_web_sm-3.8.0-py3-none-any.whl\n",
      "Collecting spacy-pkuseg<2.0.0,>=1.0.0 (from zh-core-web-sm==3.8.0)\n",
      "  Obtaining dependency information for spacy-pkuseg<2.0.0,>=1.0.0 from https://files.pythonhosted.org/packages/a0/b7/07c159266ced53b7e56d823aa3c2e304ab9726f56c214a901600c9ec94d2/spacy_pkuseg-1.0.0-cp311-cp311-win_amd64.whl.metadata\n",
      "  Using cached spacy_pkuseg-1.0.0-cp311-cp311-win_amd64.whl.metadata (13 kB)\n",
      "Requirement already satisfied: srsly<3.0.0,>=2.3.0 in c:\\users\\31873\\anaconda3\\lib\\site-packages (from spacy-pkuseg<2.0.0,>=1.0.0->zh-core-web-sm==3.8.0) (2.5.1)\n",
      "Requirement already satisfied: numpy<3.0.0,>=2.0.0 in c:\\users\\31873\\anaconda3\\lib\\site-packages (from spacy-pkuseg<2.0.0,>=1.0.0->zh-core-web-sm==3.8.0) (2.2.3)\n",
      "Requirement already satisfied: catalogue<2.1.0,>=2.0.3 in c:\\users\\31873\\anaconda3\\lib\\site-packages (from srsly<3.0.0,>=2.3.0->spacy-pkuseg<2.0.0,>=1.0.0->zh-core-web-sm==3.8.0) (2.0.10)\n",
      "Using cached spacy_pkuseg-1.0.0-cp311-cp311-win_amd64.whl (2.4 MB)\n",
      "Installing collected packages: spacy-pkuseg, zh-core-web-sm\n",
      "Successfully installed spacy-pkuseg-1.0.0 zh-core-web-sm-3.8.0\n",
      "Note: you may need to restart the kernel to use updated packages.\n"
     ]
    }
   ],
   "source": [
    "pip install C:\\Users\\31873\\AppData\\Local\\Temp\\MicrosoftEdgeDownloads\\dba00908-4796-4f7d-8a1b-37a25065bb6b\\zh_core_web_sm-3.8.0-py3-none-any.whl"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "5936ceb0",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "train size = 8627 , test size = 2157\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100%|██████████████████████████████████████████████████████████████████████████████| 8627/8627 [01:29<00:00, 96.34it/s]\n",
      "100%|█████████████████████████████████████████████████████████████████████████████| 2157/2157 [00:20<00:00, 103.54it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['python', '编程', '入门', '教程']\n",
      "{'计算机类': 0, '艺术传媒类': 1, '经管类': 2}\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "import json\n",
    "import os\n",
    "import requests\n",
    "import re\n",
    "from tqdm import tqdm\n",
    "from collections import defaultdict\n",
    "from string import punctuation\n",
    "import spacy\n",
    "from spacy.lang.zh.stop_words import STOP_WORDS\n",
    "nlp = spacy.load('zh_core_web_sm')\n",
    "\n",
    "\n",
    "class BooksDataset:\n",
    "    def __init__(self):\n",
    "        train_file, test_file = 'train.jsonl', 'test.jsonl'\n",
    "\n",
    "        # 下载数据为JSON格式，转化为Python对象\n",
    "        def read_file(file_name):\n",
    "            with open(file_name, 'r', encoding='utf-8') as fin:\n",
    "                json_list = list(fin)\n",
    "            data_split = []\n",
    "            for json_str in json_list:\n",
    "                data_split.append(json.loads(json_str))\n",
    "            return data_split\n",
    "\n",
    "        self.train_data, self.test_data = read_file(train_file),\\\n",
    "            read_file(test_file)\n",
    "        print('train size =', len(self.train_data), \n",
    "              ', test size =', len(self.test_data))\n",
    "        \n",
    "        # 建立文本标签和数字标签的映射\n",
    "        self.label2id, self.id2label = {}, {}\n",
    "        for data_split in [self.train_data, self.test_data]:\n",
    "            for data in data_split:\n",
    "                txt = data['class']\n",
    "                if txt not in self.label2id:\n",
    "                    idx = len(self.label2id)\n",
    "                    self.label2id[txt] = idx\n",
    "                    self.id2label[idx] = txt\n",
    "                label_id = self.label2id[txt]\n",
    "                data['label'] = label_id\n",
    "\n",
    "    def tokenize(self, attr='book'):\n",
    "        # 使用以下两行命令安装spacy用于中文分词\n",
    "        # pip install -U spacy\n",
    "        # python -m spacy download zh_core_web_sm\n",
    "        # 去除文本中的符号和停用词\n",
    "        for data_split in [self.train_data, self.test_data]:\n",
    "            for data in tqdm(data_split):\n",
    "                # 转为小写\n",
    "                text = data[attr].lower()\n",
    "                # 符号替换为空\n",
    "                tokens = [t.text for t in nlp(text) \\\n",
    "                    if t.text not in STOP_WORDS]\n",
    "                # 这一步比较耗时，因此把tokenize的结果储存起来\n",
    "                data['tokens'] = tokens\n",
    "\n",
    "    # 根据分词结果建立词表，忽略部分低频词，\n",
    "    # 可以设置词最短长度和词表最大大小\n",
    "    def build_vocab(self, min_freq=3, min_len=2, max_size=None):\n",
    "        frequency = defaultdict(int)\n",
    "        for data in self.train_data:\n",
    "            tokens = data['tokens']\n",
    "            for token in tokens:\n",
    "                frequency[token] += 1 \n",
    "\n",
    "        print(f'unique tokens = {len(frequency)}, '+\\\n",
    "              f'total counts = {sum(frequency.values())}, '+\\\n",
    "              f'max freq = {max(frequency.values())}, '+\\\n",
    "              f'min freq = {min(frequency.values())}')    \n",
    "\n",
    "        self.token2id = {}\n",
    "        self.id2token = {}\n",
    "        total_count = 0\n",
    "        for token, freq in sorted(frequency.items(),\\\n",
    "            key=lambda x: -x[1]):\n",
    "            if max_size and len(self.token2id) >= max_size:\n",
    "                break\n",
    "            if freq > min_freq:\n",
    "                if (min_len is None) or (min_len and \\\n",
    "                    len(token) >= min_len):\n",
    "                    self.token2id[token] = len(self.token2id)\n",
    "                    self.id2token[len(self.id2token)] = token\n",
    "                    total_count += freq\n",
    "            else:\n",
    "                break\n",
    "        print(f'min_freq = {min_freq}, min_len = {min_len}, '+\\\n",
    "              f'max_size = {max_size}, '\n",
    "              f'remaining tokens = {len(self.token2id)}, '\n",
    "              f'in-vocab rate = {total_count / sum(frequency.values())}')\n",
    "\n",
    "    # 将分词后的结果转化为数字索引\n",
    "    def convert_tokens_to_ids(self):\n",
    "        for data_split in [self.train_data, self.test_data]:\n",
    "            for data in data_split:\n",
    "                data['token_ids'] = []\n",
    "                for token in data['tokens']:\n",
    "                    if token in self.token2id:\n",
    "                        data['token_ids'].append(self.token2id[token])\n",
    "\n",
    "        \n",
    "dataset = BooksDataset()\n",
    "dataset.tokenize()\n",
    "print(dataset.train_data[0]['tokens'])\n",
    "print(dataset.label2id)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "26d79e05",
   "metadata": {},
   "source": [
    "完成分词后，对出现次数超过3次的词元建立词表，并将分词后的文档转化为词元id的序列。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "0d6b1918",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "unique tokens = 6956, total counts = 54884, max freq = 1635, min freq = 1\n",
      "min_freq = 3, min_len = 2, max_size = None, remaining tokens = 1650, in-vocab rate = 0.7944209605713869\n",
      "[18, 26, 5, 0]\n"
     ]
    }
   ],
   "source": [
    "dataset.build_vocab(min_freq=3)\n",
    "dataset.convert_tokens_to_ids()\n",
    "print(dataset.train_data[0]['token_ids'])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d096d95f",
   "metadata": {},
   "source": [
    "接下来将数据和标签准备成便于训练的矩阵格式。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "ba632265",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "train_X, train_Y = [], []\n",
    "test_X, test_Y = [], []\n",
    "\n",
    "for data in dataset.train_data:\n",
    "    x = np.zeros(len(dataset.token2id), dtype=np.int32)\n",
    "    for token_id in data['token_ids']:\n",
    "        x[token_id] += 1\n",
    "    train_X.append(x)\n",
    "    train_Y.append(data['label'])\n",
    "for data in dataset.test_data:\n",
    "    x = np.zeros(len(dataset.token2id), dtype=np.int32)\n",
    "    for token_id in data['token_ids']:\n",
    "        x[token_id] += 1\n",
    "    test_X.append(x)\n",
    "    test_Y.append(data['label'])\n",
    "train_X, train_Y = np.array(train_X), np.array(train_Y)\n",
    "test_X, test_Y = np.array(test_X), np.array(test_Y)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3938acdb",
   "metadata": {},
   "source": [
    "下面代码展示朴素贝叶斯的训练和预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "f13251b7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "P(计算机类) = 0.4453460067230787\n",
      "P(艺术传媒类) = 0.26660484525327466\n",
      "P(经管类) = 0.2880491480236467\n",
      "P(教程|计算机类) = 0.5726495726495726\n",
      "P(基础|计算机类) = 0.6503006012024048\n",
      "P(设计|计算机类) = 0.606694560669456\n",
      "test example-0, prediction = 0, label = 0\n",
      "test example-1, prediction = 0, label = 0\n",
      "test example-2, prediction = 1, label = 1\n",
      "test example-3, prediction = 1, label = 1\n",
      "test example-4, prediction = 1, label = 1\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "\n",
    "class NaiveBayes:\n",
    "    def __init__(self, num_classes, vocab_size):\n",
    "        self.num_classes = num_classes\n",
    "        self.vocab_size = vocab_size\n",
    "        self.prior = np.zeros(num_classes, dtype=np.float64)\n",
    "        self.likelihood = np.zeros((num_classes, vocab_size),\\\n",
    "            dtype=np.float64)\n",
    "        \n",
    "    def fit(self, X, Y):\n",
    "        # NaiveBayes的训练主要涉及先验概率和似然的估计，\n",
    "        # 这两者都可以通过计数简单获得\n",
    "        for x, y in zip(X, Y):\n",
    "            self.prior[y] += 1\n",
    "            for token_id in x:\n",
    "                self.likelihood[y, token_id] += 1\n",
    "                \n",
    "        self.prior /= self.prior.sum()\n",
    "        # laplace平滑\n",
    "        self.likelihood += 1\n",
    "        self.likelihood /= self.likelihood.sum(axis=0)\n",
    "        # 为了避免精度溢出，使用对数概率\n",
    "        self.prior = np.log(self.prior)\n",
    "        self.likelihood = np.log(self.likelihood)\n",
    "    \n",
    "    def predict(self, X):\n",
    "        # 算出各个类别的先验概率与似然的乘积，找出最大的作为分类结果\n",
    "        preds = []\n",
    "        for x in X:\n",
    "            p = np.zeros(self.num_classes, dtype=np.float64)\n",
    "            for i in range(self.num_classes):\n",
    "                p[i] += self.prior[i]\n",
    "                for token in x:\n",
    "                    p[i] += self.likelihood[i, token]\n",
    "            preds.append(np.argmax(p))\n",
    "        return preds\n",
    "\n",
    "nb = NaiveBayes(len(dataset.label2id), len(dataset.token2id))\n",
    "train_X, train_Y = [], []\n",
    "for data in dataset.train_data:\n",
    "    train_X.append(data['token_ids'])\n",
    "    train_Y.append(data['label'])\n",
    "nb.fit(train_X, train_Y)\n",
    "\n",
    "for i in range(3):\n",
    "    print(f'P({dataset.id2label[i]}) = {np.exp(nb.prior[i])}')\n",
    "for i in range(3):\n",
    "    print(f'P({dataset.id2token[i]}|{dataset.id2label[0]}) = '+\\\n",
    "          f'{np.exp(nb.likelihood[0, i])}')\n",
    "\n",
    "test_X, test_Y = [], []\n",
    "for data in dataset.test_data:\n",
    "    test_X.append(data['token_ids'])\n",
    "    test_Y.append(data['label'])\n",
    "    \n",
    "NB_preds = nb.predict(test_X)\n",
    "    \n",
    "for i, (p, y) in enumerate(zip(NB_preds, test_Y)):\n",
    "    if i >= 5:\n",
    "        break\n",
    "    print(f'test example-{i}, prediction = {p}, label = {y}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1cf6399",
   "metadata": {},
   "source": [
    "下面使用第3章介绍的TF-IDF方法得到文档的特征向量，并使用PyTorch实现逻辑斯谛回归模型的训练和预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "21a3bc79",
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "import os\n",
    "import sys\n",
    "\n",
    "sys.path.append('../code')\n",
    "from my_utils import TFIDF\n",
    "        \n",
    "tfidf = TFIDF(len(dataset.token2id))\n",
    "tfidf.fit(train_X)\n",
    "train_F = tfidf.transform(train_X)\n",
    "test_F = tfidf.transform(test_X)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dc8af30b",
   "metadata": {},
   "source": [
    "逻辑斯谛回归可以看作一个一层的神经网络模型，使用PyTorch实现可以方便地利用自动求导功能。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "1ddebf0c",
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "epoch-49, loss=0.1744: 100%|█| 50/50 [00:14<00:00,  3.39it/s\n"
     ]
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "eval_loss = 0.2869\n"
     ]
    }
   ],
   "source": [
    "import torch\n",
    "from torch import nn\n",
    "\n",
    "class LR(nn.Module):\n",
    "    def __init__(self, input_dim, output_dim):\n",
    "        super(LR, self).__init__()\n",
    "        self.linear = nn.Linear(input_dim, output_dim)\n",
    "        \n",
    "    def forward(self, input_feats, labels=None):\n",
    "        outputs = self.linear(input_feats)\n",
    "        \n",
    "        if labels is not None:\n",
    "            loss_fc = nn.CrossEntropyLoss()\n",
    "            loss = loss_fc(outputs, labels)\n",
    "            return (loss, outputs)\n",
    "        \n",
    "        return outputs\n",
    "\n",
    "model = LR(len(dataset.token2id), len(dataset.label2id))\n",
    "\n",
    "from torch.utils.data import Dataset, DataLoader\n",
    "from torch.optim import SGD, Adam\n",
    "\n",
    "# 使用PyTorch的DataLoader来进行数据循环，因此按照PyTorch的接口\n",
    "# 实现myDataset和DataCollator两个类\n",
    "# myDataset是对特征向量和标签的简单封装便于对齐接口，\n",
    "# DataCollator用于批量将数据转化为PyTorch支持的张量类型\n",
    "class myDataset(Dataset):\n",
    "    def __init__(self, X, Y):\n",
    "        self.X = X\n",
    "        self.Y = Y\n",
    "        \n",
    "    def __len__(self):\n",
    "        return len(self.X)\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        return (self.X[idx], self.Y[idx])\n",
    "\n",
    "class DataCollator:\n",
    "    @classmethod\n",
    "    def collate_batch(cls, batch):\n",
    "        feats, labels = [], []\n",
    "        for x, y in batch:\n",
    "            feats.append(x)\n",
    "            labels.append(y)\n",
    "        # 直接将一个ndarray的列表转化为张量是非常慢的，\n",
    "        # 所以需要提前将列表转化为一整个ndarray\n",
    "        feats = torch.tensor(np.array(feats), dtype=torch.float)\n",
    "        labels = torch.tensor(np.array(labels), dtype=torch.long)\n",
    "        return {'input_feats': feats, 'labels': labels}\n",
    "\n",
    "# 设置训练超参数和优化器，模型初始化\n",
    "epochs = 50\n",
    "batch_size = 128\n",
    "learning_rate = 1e-3\n",
    "weight_decay = 0\n",
    "\n",
    "train_dataset = myDataset(train_F, train_Y)\n",
    "test_dataset = myDataset(test_F, test_Y)\n",
    "\n",
    "data_collator = DataCollator()\n",
    "train_dataloader = DataLoader(train_dataset, batch_size=batch_size,\\\n",
    "    shuffle=True, collate_fn=data_collator.collate_batch)\n",
    "test_dataloader = DataLoader(test_dataset, batch_size=batch_size,\\\n",
    "    shuffle=False, collate_fn=data_collator.collate_batch)\n",
    "optimizer = Adam(model.parameters(), lr=learning_rate,\\\n",
    "    weight_decay=weight_decay)\n",
    "model.zero_grad()\n",
    "model.train()\n",
    "\n",
    "from tqdm import tqdm, trange\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# 模型训练\n",
    "with trange(epochs, desc='epoch', ncols=60) as pbar:\n",
    "    epoch_loss = []\n",
    "    for epoch in pbar:\n",
    "        model.train()\n",
    "        for step, batch in enumerate(train_dataloader):\n",
    "            loss = model(**batch)[0]\n",
    "            pbar.set_description(f'epoch-{epoch}, loss={loss.item():.4f}')\n",
    "            loss.backward()\n",
    "            optimizer.step()\n",
    "            model.zero_grad()\n",
    "            epoch_loss.append(loss.item())\n",
    "\n",
    "    epoch_loss = np.array(epoch_loss)\n",
    "    # 打印损失曲线\n",
    "    plt.plot(range(len(epoch_loss)), epoch_loss)\n",
    "    plt.xlabel('training epoch')\n",
    "    plt.ylabel('loss')\n",
    "    plt.show()\n",
    "    \n",
    "    model.eval()\n",
    "    with torch.no_grad():\n",
    "        loss_terms = []\n",
    "        for batch in test_dataloader:\n",
    "            loss = model(**batch)[0]\n",
    "            loss_terms.append(loss.item())\n",
    "        print(f'eval_loss = {np.mean(loss_terms):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10808854",
   "metadata": {},
   "source": [
    "下面的代码使用训练好的模型对测试集进行预测，并报告分类结果。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "11a9bf62",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "test example-0, prediction = 0, label = 0\n",
      "test example-1, prediction = 0, label = 0\n",
      "test example-2, prediction = 1, label = 1\n",
      "test example-3, prediction = 1, label = 1\n",
      "test example-4, prediction = 1, label = 1\n"
     ]
    }
   ],
   "source": [
    "LR_preds = []\n",
    "model.eval()\n",
    "for batch in test_dataloader:\n",
    "    with torch.no_grad():\n",
    "        _, preds = model(**batch)\n",
    "        preds = np.argmax(preds, axis=1)\n",
    "        LR_preds.extend(preds)\n",
    "            \n",
    "for i, (p, y) in enumerate(zip(LR_preds, test_Y)):\n",
    "    if i >= 5:\n",
    "        break\n",
    "    print(f'test example-{i}, prediction = {p}, label = {y}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c5feb65e",
   "metadata": {},
   "source": [
    "下面的代码展示多分类情况下宏平均和微平均的算法。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "a5ac32c5",
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "NB: micro-f1 = 0.8961520630505331, macro-f1 = 0.8948572078813896\n",
      "LR: micro-f1 = 0.914696337505795, macro-f1 = 0.91405580603299\n"
     ]
    }
   ],
   "source": [
    "test_Y = np.array(test_Y)\n",
    "NB_preds = np.array(NB_preds)\n",
    "LR_preds = np.array(LR_preds)\n",
    "\n",
    "def micro_f1(preds, labels):\n",
    "    TP = np.sum(preds == labels)\n",
    "    FN = FP = 0\n",
    "    for i in range(len(dataset.label2id)):\n",
    "        FN += np.sum((preds == i) & (labels != i))\n",
    "        FP += np.sum((preds != i) & (labels == i))\n",
    "    precision = TP / (TP + FP)\n",
    "    recall = TP / (TP + FN)\n",
    "    f1 = 2 * precision * recall / (precision + recall)\n",
    "    return f1\n",
    "\n",
    "def macro_f1(preds, labels):\n",
    "    f_scores = []\n",
    "    for i in range(len(dataset.label2id)):\n",
    "        TP = np.sum((preds == i) & (labels == i))\n",
    "        FN = np.sum((preds == i) & (labels != i))\n",
    "        FP = np.sum((preds != i) & (labels == i))\n",
    "        precision = TP / (TP + FP)\n",
    "        recall = TP / (TP + FN)\n",
    "        f1 = 2 * precision * recall / (precision + recall)\n",
    "        f_scores.append(f1)\n",
    "    return np.mean(f_scores)\n",
    "\n",
    "print(f'NB: micro-f1 = {micro_f1(NB_preds, test_Y)}, '+\\\n",
    "      f'macro-f1 = {macro_f1(NB_preds, test_Y)}')\n",
    "print(f'LR: micro-f1 = {micro_f1(LR_preds, test_Y)}, '+\\\n",
    "      f'macro-f1 = {macro_f1(LR_preds, test_Y)}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8d3879dd",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
